package com.tmt.datatable.helper;

import com.tmt.annotation.parser.EnumAnnotationParser;
import com.tmt.datatable.TableRequest;
import com.tmt.datatable.TableResponse;
import com.tmt.datatable.model.Column;
import com.tmt.helper.CollectionHelper;
import com.tmt.helper.DateHelper;
import com.tmt.helper.ReflectHelper;
import com.tmt.helper.StringHelper;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.expression.spel.ast.Literal;
import org.springframework.util.StringUtils;

import javax.persistence.criteria.*;
import java.io.Serializable;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Created by ThreeManTeam on 2017/6/8.
 */
public class TableRequestHelper {

    //基本类列表。
    public static List<Class> BasicClass = Arrays.asList(
            String.class,
            Byte.class, byte.class,
            Short.class, short.class,
            Integer.class, int.class,
            Float.class, float.class,
            Double.class, double.class,
            Long.class, long.class,
            Boolean.class, boolean.class,
            BigDecimal.class
    );

    /**
     * 依据请求查询数据。
     *
     * @param request                  Http请求
     * @param clazz                    要查询的实体类型
     * @param searcherConditions       额外附加的查询条件，存放的是以属性名称为键，以属性值为值的一个Map
     * @param filterConditions         额外附加的查询条件，存放的是以属性名称为键，以属性值为值的一个Map
     * @param limitedConditions        额外附加的查询条件，存放的是以属性名称为键，以属性值为值的一个Map     *
     * @param conditionParser          条件分析类对象。
     * @param jpaSpecificationExecutor
     * @param <T>
     * @return
     */
    public <T extends Serializable> TableResponse<T> queryByRequest(TableRequest request, Class<T> clazz,
                                                                    Map<String, String[]> searcherConditions,
                                                                    Map<String, String[]> filterConditions,
                                                                    Map<String, String[]> limitedConditions,
                                                                    QueryConditionParser conditionParser,
                                                                    JpaSpecificationExecutor<T> jpaSpecificationExecutor) {
        Specification<T> s = tableCondition(request, searcherConditions, filterConditions, limitedConditions, clazz, conditionParser);
        Pageable a = pageByRequest(request);
        Page<T> data = jpaSpecificationExecutor.findAll(s, a);
        return TableResponse.fromPage(data);
    }

    public static Pageable pageByRequest(TableRequest request) {
        int start = request.getStart();
        int length = request.getLength();
        int page = Math.floorDiv(start, length);
        return new PageRequest(page, request.getLength());
    }

    private <T> Specification<T> tableCondition(TableRequest request,
                                                Map<String, String[]> searcherConditions,
                                                Map<String, String[]> filterConditions,
                                                Map<String, String[]> limitedConditions,
                                                Class<T> clazz, QueryConditionParser conditionParser) {
        if (request == null)
            return null;

        Map<String, String[]> filters = new HashMap<>();
        filters.putAll(request.getFilter());

        //filterConditions的合并不多余。
        if (filterConditions != null)
            filterConditions.keySet().stream().forEach(x -> {
                if (filters.keySet().stream().noneMatch(y -> Objects.equals(y, x)))
                    filters.put(x, filterConditions.get(x));
            });

        //limitedConditions条件级别最高，所以用limitedConditions的项替换filters中的现有项。
        if (limitedConditions != null) {
            limitedConditions.keySet().stream().forEach(x -> {
                filters.remove(x);
                filters.put(x, limitedConditions.get(x));
            });
        }

        //构造搜索过滤条件,条件之间为“或/OR”关系。
        Map<String, String[]> searchFilter = new HashMap<>();
        if (request.getSearch() != null) {
            if (!StringUtils.isEmpty(request.getSearch().getValue())) {
                List<String> validColumns = request.getColumns().stream()
                        .filter(Column::getSearchable)
                        .map(Column::getData)
                        .collect(Collectors.toList());
                appendSearchConditions(clazz, validColumns, request.getSearch().getValue(), searchFilter);
            }
        }
        if (searcherConditions != null && !searcherConditions.isEmpty()) {
            searcherConditions.keySet().forEach(x -> {
                searchFilter.remove(x);
                searchFilter.put(x, searcherConditions.get(x));
            });
        }

        Specification<T> condition = new Specification<T>() {
            @Override
            public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                List<Predicate> predicateList = new ArrayList<>();
                // 过滤掉主键为空，值域为空，或者数组第一个对象为空的
                filters.entrySet().stream()
                        .filter(c -> !StringHelper.isEmpty(c.getKey())
                                && !CollectionHelper.isEmpty(c.getValue())
                        )
                        .forEach(c -> {
                            if (conditionParser == null || !conditionParser.parserConditon(clazz, root, query, cb, predicateList, c)) {
                                defaultTableConditionHandle(root, query, cb, clazz, c, predicateList);
                            }
                        });

                //获取搜索条件。
                List<Predicate> searchPredicateList = new ArrayList<>();
                searchFilter.entrySet().forEach(x -> {
                    if (conditionParser == null || !conditionParser.parserConditon(clazz, root, query, cb, predicateList, x)) {
                        defaultTableConditionHandle(root, query, cb, clazz, x, searchPredicateList);
                    }
                });

                //条件之间为“与/AND”关系。
                Predicate condition1 = predicateList.isEmpty() ? cb.conjunction()
                        : cb.and(predicateList.toArray(new Predicate[predicateList.size()]));

                //条件之间为““与/AND”关系。
                Predicate condition2 = searchPredicateList.isEmpty() ? cb.conjunction()
                        : cb.or(searchPredicateList.toArray(new Predicate[searchPredicateList.size()]));

                Predicate predicate = cb.and(condition1, condition2);

                List<Order> orders = orderByRequest(clazz, root, query, cb, request);

                return query.where(predicate).distinct(true).orderBy(orders).getRestriction();
            }
        };
        return condition;
    }

    private <T> List<Order> orderByRequest(Class<T> clazz, Root root, CriteriaQuery cq, CriteriaBuilder cb, TableRequest request) {
        List<Order> resultList = new LinkedList<>();
        List<Column> columnList = request.getColumns();
        List<com.tmt.datatable.model.Order> orderList = request.getOrder();

        for (int i = 0, j = orderList.size(); i < j; i++) {
            try {
                com.tmt.datatable.model.Order temp = orderList.get(i);
                int orderIndex = temp.getColumn();
                Column orderColumn = columnList.get(orderIndex);
                String columnName = orderColumn.getData();

                Path path = null;
                Class<?> lastClazz = null;
                if (columnName.contains(".")) {
                    String[] columnArray = columnName.split("\\.");
                    for (String s : columnArray) {
                        if (path == null) {
                            path = root.get(s);
                            lastClazz = ReflectHelper.getFieldClassByGetterMethod(clazz, s);
                        } else {
                            path = path.get(s);
                            lastClazz = ReflectHelper.getFieldClassByGetterMethod(lastClazz, s);
                        }
                    }
                } else {
                    path = root.get(columnName);
                    lastClazz = ReflectHelper.getFieldClassByGetterMethod(clazz, columnName);
                }

                // 排序不区分大小写
                if (Objects.equals(temp.getDir(), com.tmt.datatable.model.Order.ASC)) {
                    resultList.add(cb.asc(path.as(lastClazz)));
                } else if (Objects.equals(temp.getDir(), com.tmt.datatable.model.Order.DESC)) {
                    resultList.add(cb.desc(path.as(lastClazz)));
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }

        return resultList;
    }

    public static <T> void defaultTableConditionHandle(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb, Class<T> clazz, Map.Entry<String, String[]> entry, List<Predicate> predicateList) {
        String key = entry.getKey();
        String[] value = entry.getValue();

        Class fieldClazz = ReflectHelper.getActualClassByFieldPath(clazz, key, true, false);
        if (fieldClazz == null)
            return;

        Path<?> path = SpecificationHelper.path(root, key, false);
        if (path == null)
            return;

        List<Predicate> tmpPredicateList = new ArrayList<>();
        if (key.contains(".")) {
            Path<?> parentPath = SpecificationHelper.path(root, key, true);
            Predicate notNull = cb.isNotNull(parentPath);
            tmpPredicateList.add(notNull);
        }

        Predicate p = null;
        try {
            switch (fieldClazz.getSimpleName()) {
                case "String":
                    if (value.length > 1) {
                        CriteriaBuilder.In<String> in = cb.in(path.as(String.class));
                        Arrays.stream(value).forEach(in::value);
                        p = in;
                    } else {
                        String tmpValue = value[0];
                        if (!StringUtils.isEmpty(tmpValue)) {
                            if (tmpValue.contains(",")) {
                                CriteriaBuilder.In<String> in = cb.in(path.as(String.class));
                                Arrays.stream(value[0].split(",")).forEach(in::value);
                                p = in;
                            } else {
                                if (!tmpValue.startsWith("%"))
                                    tmpValue = "%" + tmpValue;

                                if (!tmpValue.endsWith("%"))
                                    tmpValue = tmpValue + "%";

                                p = cb.like(path.as(String.class), tmpValue);
                            }
                        } else {
                            //将值做为null处理。
                            p = cb.equal(path.as(String.class), Literal.NULL);
                        }
                    }
                    break;
                case "Date":
                    String tmp = !StringUtils.isEmpty(value[0]) ? value[0] : "1900-01-01";
                    Date start = StringHelper.convertTo(tmp, Date.class);
                    p = cb.greaterThanOrEqualTo(path.as(Date.class), start);
                    if (value.length > 1) {
                        tmp = !StringUtils.isEmpty(value[1]) ? value[1]
                                : new SimpleDateFormat("yyyy-MM-dd").format(DateHelper.addDays(new Date(), 1));
                        Date end = StringHelper.convertTo(tmp, Date.class);
                        p = cb.between(path.as(Date.class), start, end);
                    }
                    break;
                case "Byte":
                case "byte":
                case "Boolean":
                case "boolean":
                    Object obj = StringHelper.convertTo(value[0], fieldClazz);
                    if (obj != null) {
                        p = cb.equal(path, obj);
                    }
                    break;
                default:
                    //特殊处理枚举类型
                    if (fieldClazz.isEnum() || BasicClass.contains(fieldClazz)) {
                        switch (value.length) {
                            case 1:
                                List<String> infoArray = new ArrayList<>();
                                infoArray.addAll(Arrays.stream(value[0].split(",")).collect(Collectors.toList()));

                                List<Predicate> tmpList = new ArrayList<>();
                                infoArray.forEach(x -> {
                                    Object objValue = StringHelper.convertTo(x, fieldClazz);
                                    if (objValue != null) {
                                        Predicate tmpPre = cb.equal(path.as(fieldClazz), objValue);
                                        tmpList.add(tmpPre);
                                    }
                                });
                                p = tmpList.isEmpty() ? cb.conjunction()
                                        : cb.or(tmpList.toArray(new Predicate[tmpList.size()]));
                                break;
                            case 2:
                                Object range1 = StringHelper.convertTo(value[0], fieldClazz);
                                Object range2 = StringHelper.convertTo(value[1], fieldClazz);
                                if (range1 != null && range2 != null) {
                                    //大于等于range1,且小于range2
                                    p = createPredicate(cb, path, Arrays.asList(range1, range2), false);
                                }
                                break;
                            default:
                                List<Object> range = new ArrayList<>();
                                Arrays.asList(value).stream().forEach(x -> {
                                    try {
                                        Object item = StringHelper.convertTo(x, fieldClazz);
                                        if (item != null && !range.contains(item)) {
                                            range.add(item);
                                        }
                                    } catch (Exception ex) {
                                    }
                                });
                                if (range.size() > 0) {
                                    //in Range列表中。
                                    p = createPredicate(cb, path, range, true);
                                }
                                break;
                        }
                    } else {
                        switch (value.length) {
                            case 1:
                                p = !StringUtils.isEmpty(value[0]) ? cb.isNotNull(path) : cb.isNull(path);
                                break;
                        }
                    }
                    break;
            }
        } catch (Exception ex) {
        }

        if (p != null)
            tmpPredicateList.add(p);

        Predicate condition = tmpPredicateList.isEmpty() ? cb.conjunction()
                : cb.and(tmpPredicateList.toArray(new Predicate[tmpPredicateList.size()]));

        predicateList.add(condition);
    }

    /**
     * 将Search条件转换为过滤条件。
     * 类型可搜索字段自动匹配。
     *
     * @param clazz     目标类
     * @param propNames 目标类的属性名称列表
     * @param search    要搜索匹配的值
     * @param filters   输出的字段过滤条件
     * @param <T>
     */
    public static <T> void appendSearchConditions(Class<T> clazz, List<String> propNames, String search, Map<String, String[]> filters) {
        if (StringUtils.isEmpty(search))
            return;

        propNames.parallelStream().forEach(x -> {
            try {
                if (filters.keySet().stream().anyMatch(y -> Objects.equals(y, x)))
                    return;

                //获取字段类型。
                Class<?> fieldClazz = ReflectHelper.getActualClassByFieldPath(clazz, x, true, false);
                if (fieldClazz == null)
                    return;

                switch (fieldClazz.getSimpleName()) {
                    case "Date":
                        String[] dateRange = getDateRange(search);
                        if (dateRange != null)
                            filters.put(x, dateRange);
                        break;
                    default:
                        //特殊处理枚举类型
                        if (fieldClazz.isEnum()) {
                            String name = EnumAnnotationParser.getNameFromTitle(fieldClazz, search);
                            if (name != null)
                                filters.put(x, new String[]{name});
                        } else if (BasicClass.contains(fieldClazz)) {
                            Object obj = StringHelper.convertTo(search, fieldClazz);
                            if (obj != null)
                                filters.put(x, new String[]{search});
                        }
                        break;
                }
            } catch (Exception ex) {
            }
        });
    }

    /**
     * 创建Predicate查询对象。
     *
     * @param cb
     * @param path
     * @param range
     * @param inOperation 是否采用In操作。
     * @return
     */
    private static Predicate createPredicate(CriteriaBuilder cb, Path<?> path, List<Object> range, boolean inOperation) {
        Class<?> clazz = range.get(0).getClass();

        Predicate result = null;
        if (clazz.isEnum() || inOperation) {
            List<Predicate> tmpList = new ArrayList<>();
            for (Object aRange : range) tmpList.add(cb.equal(path, aRange));
            result = tmpList.isEmpty() ? cb.conjunction()
                    : cb.or(tmpList.toArray(new Predicate[tmpList.size()]));
            return result;
        }

        switch (clazz.getSimpleName()) {
            case "Short":
            case "short":
                if (range.size() == 2) {
                    Predicate p1 = cb.greaterThanOrEqualTo(path.as(Short.class), (Short) range.get(0));
                    Predicate p2 = cb.lessThanOrEqualTo(path.as(Short.class), (Short) range.get(1));
                    result = cb.and(p1, p2);
                }
                break;
            case "Integer":
            case "int":
                if (range.size() == 2) {
                    Predicate p1 = cb.greaterThanOrEqualTo(path.as(Integer.class), (Integer) range.get(0));
                    Predicate p2 = cb.lessThanOrEqualTo(path.as(Integer.class), (Integer) range.get(1));
                    result = cb.and(p1, p2);
                }
                break;
            case "Float":
            case "float":
                if (range.size() == 2) {
                    Predicate p1 = cb.greaterThanOrEqualTo(path.as(Float.class), (Float) range.get(0));
                    Predicate p2 = cb.lessThanOrEqualTo(path.as(Float.class), (Float) range.get(1));
                    result = cb.and(p1, p2);
                }
                break;
            case "Double":
            case "double":
                if (range.size() == 2) {
                    Predicate p1 = cb.greaterThanOrEqualTo(path.as(Double.class), (Double) range.get(0));
                    Predicate p2 = cb.lessThanOrEqualTo(path.as(Double.class), (Double) range.get(1));
                    result = cb.and(p1, p2);
                }
                break;
            case "Long":
            case "long":
                if (range.size() == 2) {
                    Predicate p1 = cb.greaterThanOrEqualTo(path.as(Long.class), (Long) range.get(0));
                    Predicate p2 = cb.lessThanOrEqualTo(path.as(Long.class), (Long) range.get(1));
                    result = cb.and(p1, p2);
                }
                break;
            case "BigDecimal":
                if (range.size() == 2) {
                    Predicate p1 = cb.greaterThanOrEqualTo(path.as(BigDecimal.class), (BigDecimal) range.get(0));
                    Predicate p2 = cb.lessThanOrEqualTo(path.as(BigDecimal.class), (BigDecimal) range.get(1));
                    result = cb.and(p1, p2);
                }
                break;
        }
        return result;
    }

    /**
     * 根据时间字符串给出时间范围字符串。
     *
     * @param date 时间字符串，格式“yyyy-MM-dd HH:mm:ss”
     * @return
     */
    private static String[] getDateRange(String date) {
        if (StringUtils.isEmpty(date.trim()))
            return null;

        date = date.trim();

        String[] dateRange = null;
        try {
            SimpleDateFormat sdf = null;
            Date start = null;
            Date end = null;
            switch (date.length()) {
                case 4:
                    sdf = new SimpleDateFormat("yyyy");
                    start = sdf.parse(date);
                    end = DateHelper.addYears(start, 1);
                    break;
                case 7:
                    sdf = new SimpleDateFormat("yyyy-MM");
                    start = sdf.parse(date);
                    end = DateHelper.addMonths(start, 1);
                    break;
                case 10:
                    sdf = new SimpleDateFormat("yyyy-MM-dd");
                    start = sdf.parse(date);
                    end = DateHelper.addDays(start, 1);
                    break;
                case 12:
                case 13:
                    sdf = new SimpleDateFormat("yyyy-MM-dd HH");
                    start = sdf.parse(date);
                    end = DateHelper.addHours(start, 1);
                    break;
                case 15:
                case 16:
                    sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
                    start = sdf.parse(date);
                    end = DateHelper.addMinutes(start, 1);
                    break;
                case 18:
                case 19:
                    sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    start = sdf.parse(date);
                    end = DateHelper.addSeconds(start, 1);
                    break;
            }
            if (sdf != null && start != null && end != null) {
                dateRange = new String[2];
                dateRange[0] = sdf.format(start);
                dateRange[1] = sdf.format(end);
            }
        } catch (Exception ex) {
            dateRange = null;
        }
        return dateRange;
    }
}
